﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Bovril.PlayingCards.French;

namespace Bovril.Poker
{
    public class HoldEmAnalyser
    {
        public HoldEmAnalyser()
        {
            cachedBestHandsFrom7_ = new Dictionary<long, Hand>();

            holeArchetypes_ = new HoleArchetype[0];
            holeFlopPercentileMap_ = new Dictionary<int, double>();
            holeTurnPercentileMap_ = new Dictionary<long, double>();
            holeRiverPercentileMap_ = new Dictionary<long, double>();
        }

        private HoldEmAnalyser(BinaryReader binaryReader)
        {
            cachedBestHandsFrom7_ = new Dictionary<long, Hand>();

            int dataVersion = binaryReader.ReadInt32();
            if (dataVersion > currentDataVersion_)
                throw new FormatException("Invalid HoldEmAnalyser data version");
            int numHoleArchetypes = binaryReader.ReadInt32();
            holeArchetypes_ = new HoleArchetype[numHoleArchetypes];
            for (int holeArchetypeIndex = 0; holeArchetypeIndex < numHoleArchetypes; ++holeArchetypeIndex)
                holeArchetypes_[holeArchetypeIndex] = HoleArchetype.Load(binaryReader);

            holeFlopPercentileMap_ = new Dictionary<int, double>();
            holeTurnPercentileMap_ = new Dictionary<long, double>();
            holeRiverPercentileMap_ = new Dictionary<long, double>();
            if (dataVersion == 2)
            {
                int numHoleFlopPercentiles = binaryReader.ReadInt32();
                for (int holeFlopPercentileIndex = 0; holeFlopPercentileIndex < numHoleFlopPercentiles; ++holeFlopPercentileIndex)
                {
                    int hash = binaryReader.ReadInt32();
                    double percentile = binaryReader.ReadDouble();
                    holeFlopPercentileMap_.Add(hash, percentile);
                }

                int numHoleTurnPercentiles = binaryReader.ReadInt32();
                for (int holeTurnPercentileIndex = 0; holeTurnPercentileIndex < numHoleTurnPercentiles; ++holeTurnPercentileIndex)
                {
                    long hash = binaryReader.ReadInt64();
                    double percentile = binaryReader.ReadDouble();
                    holeTurnPercentileMap_.Add(hash, percentile);
                }

                int numHoleRiverPercentiles = binaryReader.ReadInt32();
                for (int holeRiverPercentileIndex = 0; holeRiverPercentileIndex < numHoleRiverPercentiles; ++holeRiverPercentileIndex)
                {
                    long hash = binaryReader.ReadInt64();
                    double percentile = binaryReader.ReadDouble();
                    holeRiverPercentileMap_.Add(hash, percentile);
                }
            }
        }

        public static HoldEmAnalyser Load(String pathname)
        {
            if (File.Exists(pathname))
            {
                using (BinaryReader binaryReader = new BinaryReader(File.OpenRead(pathname)))
                {
                    return new HoldEmAnalyser(binaryReader);
                }
            }
            else
            {
                HoldEmAnalyser newAnalyser = new HoldEmAnalyser();
                newAnalyser.CreateAnalyserData();
                newAnalyser.Save(pathname);

                return newAnalyser;
            }
        }

        public void Save(String pathname)
        {
            using (BinaryWriter binaryWriter = new BinaryWriter(File.Create(pathname)))
            {
                binaryWriter.Write(currentDataVersion_);

                binaryWriter.Write(holeArchetypes_.Length);
                foreach (HoleArchetype holeArchetype in holeArchetypes_)
                    holeArchetype.Save(binaryWriter);

                binaryWriter.Write(holeFlopPercentileMap_.Count);
                foreach (KeyValuePair<int, double> handFlopPercentilePair in holeFlopPercentileMap_)
                {
                    binaryWriter.Write(handFlopPercentilePair.Key);
                    binaryWriter.Write(handFlopPercentilePair.Value);
                }

                binaryWriter.Write(holeTurnPercentileMap_.Count);
                foreach (KeyValuePair<long, double> handTurnPercentilePair in holeTurnPercentileMap_)
                {
                    binaryWriter.Write(handTurnPercentilePair.Key);
                    binaryWriter.Write(handTurnPercentilePair.Value);
                }

                binaryWriter.Write(holeRiverPercentileMap_.Count);
                foreach (KeyValuePair<long, double> handRiverPercentilePair in holeRiverPercentileMap_)
                {
                    binaryWriter.Write(handRiverPercentilePair.Key);
                    binaryWriter.Write(handRiverPercentilePair.Value);
                }
            }
        }

        private void CreateAnalyserData()
        {
            Card[] deckCards = new Card[52];
            int cardIndex = 0;
            for (int suit = SuitValue.MinValue; suit <= SuitValue.MaxValue; ++suit)
            {
                for (int rank = RankValue.MinValue; rank <= RankValue.MaxValue; ++rank)
                {
                    deckCards[cardIndex++] = new Card(rank, suit);
                }
            }

            Dictionary<HoleArchetype, List<Hole>> holeArchtypeMap = new Dictionary<HoleArchetype, List<Hole>>();
            for (int card1Index = 0; card1Index < 51; ++card1Index)
            {
                for (int card2Index = card1Index + 1; card2Index < 52; ++card2Index)
                {
                    Hole hole = new Hole(deckCards[card1Index], deckCards[card2Index]);
                    HoleArchetype holeArchetype = hole.GetArchetype();
                    if (!holeArchtypeMap.ContainsKey(holeArchetype))
                        holeArchtypeMap.Add(holeArchetype, new List<Hole>());
                    holeArchtypeMap[holeArchetype].Add(hole);
                }
            }

            foreach (KeyValuePair<HoleArchetype, List<Hole>> holeArchtypePair in holeArchtypeMap)
            {
                holeArchtypePair.Key.WinPercentile = GetEstimatedHolePercentile(holeArchtypePair.Value[0], numRiverSamples_);
            }

            holeArchetypes_ = holeArchtypeMap.Keys.OrderByDescending(holeArchetype => holeArchetype.WinPercentile).ToArray();
            for (int i = 0; i < holeArchetypes_.Length; ++i)
                holeArchetypes_[i].OrderIndex = i;
        }

        public HoleArchetype GetHoleArchetype(Hole hole)
        {
            HoleArchetype holeArchetype = new HoleArchetype(hole);
            return holeArchetypes_.First(ha => ha.Equals(holeArchetype));
        }

        public double GetEstimatedHolePercentile(Hole hole, int numRiverSamples)
        {
            // 1) build up a full list of remaining deck cards
            Card[] deckCards = new Card[50];
            int cardIndex = 0;
            for (int suit = SuitValue.MinValue; suit <= SuitValue.MaxValue; ++suit)
            {
                for (int rank = RankValue.MinValue; rank <= RankValue.MaxValue; ++rank)
                {
                    Card newCard = new Card(rank, suit);
                    if (!newCard.Equals(hole.Card1) && !newCard.Equals(hole.Card2))
                    {
                        deckCards[cardIndex++] = new Card(rank, suit);
                    }
                }
            }

            // 2) Find the best hand for some sample rivers
            double bestHandPercentileSum = 0;
            for (int riverSampleIndex = 0; riverSampleIndex < numRiverSamples; ++riverSampleIndex)
            {
                List<int> deckCardIndices = new List<int>();
                for (int i = 0; i < 5; ++i)
                {
                    int deckCardIndex = random_.Next(0, deckCards.Length);
                    while(deckCardIndices.Contains(deckCardIndex))
                        deckCardIndex = random_.Next(0, deckCards.Length);
                    deckCardIndices.Add(deckCardIndex);
                }
                River possibleRiver = new River(
                    deckCards[deckCardIndices[0]],
                    deckCards[deckCardIndices[1]],
                    deckCards[deckCardIndices[2]],
                    deckCards[deckCardIndices[3]],
                    deckCards[deckCardIndices[4]]);

                Hand bestHand = GetBestHand(hole, possibleRiver);

                // Now we need to build a list of all potential rival hands
                Card[] remainingCards = new Card[45];
                int remainingCardIndex = 0;
                for (int suit = SuitValue.MinValue; suit <= SuitValue.MaxValue; ++suit)
                {
                    for (int rank = RankValue.MinValue; rank <= RankValue.MaxValue; ++rank)
                    {
                        Card newCard = new Card(rank, suit);
                        if (!hole.Contains(newCard) && !possibleRiver.Contains(newCard))
                        {
                            remainingCards[remainingCardIndex++] = new Card(rank, suit);
                        }
                    }
                }

                Hole[] rivalHoles = new Hole[(45 * 44) / (2 * 1)];
                int rivalHoleIndex = 0;
                for (int card0Index = 0; card0Index < 44; ++card0Index)
                {
                    for (int card1Index = card0Index + 1; card1Index < 45; ++card1Index)
                    {
                        rivalHoles[rivalHoleIndex++] = new Hole(remainingCards[card0Index], remainingCards[card1Index]);
                    }
                }

                Hand[] allPossibleHands = new Hand[rivalHoles.Length + 1];
                int allPossibleHandsIndex = 0;
                foreach (Hole rivalHole in rivalHoles)
                {
                    allPossibleHands[allPossibleHandsIndex++] = GetBestHand(rivalHole, possibleRiver);
                }
                allPossibleHands[allPossibleHandsIndex] = bestHand;

                Array.Sort(allPossibleHands);

                // Get the index of our hand and thus calculate the percentile
                int bestHandIndex = -1;
                for (int handIndex = 0; handIndex < allPossibleHands.Length; ++handIndex)
                {
                    if (allPossibleHands[handIndex] == bestHand)
                    {
                        bestHandIndex = handIndex;
                        break;
                    }
                }

                double bestHandPercentile = (double)bestHandIndex / (double)(allPossibleHands.Length - 1);
                bestHandPercentileSum += bestHandPercentile;
            }

            double bestHandPercentileAverage = bestHandPercentileSum / (double)(numRiverSamples);

            return bestHandPercentileAverage;
        }

        public double GetHoleFlopPercentile(Hole hole, Flop flop)
        {
            Card[] cardsUsed = new Card[5]
            {
                hole.Card1,
                hole.Card2,
                flop.Card1,
                flop.Card2,
                flop.Card3
            };

            Array.Sort(cardsUsed, new CardComparer());

            int holeFlopHash = GetFiveCardHash(cardsUsed[0], cardsUsed[1], cardsUsed[2], cardsUsed[3], cardsUsed[4]);
            if (holeFlopPercentileMap_.ContainsKey(holeFlopHash))
                return holeFlopPercentileMap_[holeFlopHash];

            // 1) build up a full list of remaining deck cards
            Card[] deckCards = new Card[47];
            int cardIndex = 0;
            for (int suit = SuitValue.MinValue; suit <= SuitValue.MaxValue; ++suit)
            {
                for (int rank = RankValue.MinValue; rank <= RankValue.MaxValue; ++rank)
                {
                    Card newCard = new Card(rank, suit);
                    if (!cardsUsed.Contains(newCard, new CardEqualityComparer()))
                        deckCards[cardIndex++] = newCard;
                }
            }

            // 2) Build the possible rivers
            River[] possibleRivers = new River[(47 * 46) / 2];// we have 3 of 5, so 47 C 2
            int possibleRiverIndex = 0;
            for (int card0Index = 0; card0Index < 46; ++card0Index)
            {
                for (int card1Index = card0Index + 1; card1Index < 47; ++card1Index)
                {
                    possibleRivers[possibleRiverIndex++] = new River(flop.Card1, flop.Card2, flop.Card3, deckCards[card0Index], deckCards[card1Index]);
                }
            }

            // 3) Find the best hand for the possible completed rivers
            double bestHandPercentileSum = 0;
            foreach(River possibleRiver in possibleRivers)
            {
                 Hand bestHand = GetBestHand(hole, possibleRiver);

                // Now we need to build a list of all potential rival hands
                Card[] remainingCards = new Card[45];
                int remainingCardIndex = 0;
                for (int suit = SuitValue.MinValue; suit <= SuitValue.MaxValue; ++suit)
                {
                    for (int rank = RankValue.MinValue; rank <= RankValue.MaxValue; ++rank)
                    {
                        Card newCard = new Card(rank, suit);
                        if (!hole.Contains(newCard) && !possibleRiver.Contains(newCard))
                        {
                            remainingCards[remainingCardIndex++] = new Card(rank, suit);
                        }
                    }
                }

                Hole[] rivalHoles = new Hole[(45 * 44) / (2 * 1)];
                int rivalHoleIndex = 0;
                for (int card0Index = 0; card0Index < 44; ++card0Index)
                {
                    for (int card1Index = card0Index + 1; card1Index < 45; ++card1Index)
                    {
                        rivalHoles[rivalHoleIndex++] = new Hole(remainingCards[card0Index], remainingCards[card1Index]);
                    }
                }

                Hand[] allPossibleHands = new Hand[rivalHoles.Length + 1];
                int allPossibleHandsIndex = 0;
                foreach (Hole rivalHole in rivalHoles)
                {
                    allPossibleHands[allPossibleHandsIndex++] = GetBestHand(rivalHole, possibleRiver);
                }
                allPossibleHands[allPossibleHandsIndex] = bestHand;

                Array.Sort(allPossibleHands);

                // Get the index of our hand and thus calculate the percentile
                int bestHandIndex = -1;
                for (int handIndex = 0; handIndex < allPossibleHands.Length; ++handIndex)
                {
                    if (allPossibleHands[handIndex] == bestHand)
                    {
                        bestHandIndex = handIndex;
                        break;
                    }
                }

                double bestHandPercentile = (double)bestHandIndex / (double)(allPossibleHands.Length - 1);
                bestHandPercentileSum += bestHandPercentile;
            }

            double bestHandPercentileAverage = bestHandPercentileSum / (double)(possibleRivers.Length);

            
            holeFlopPercentileMap_.Add(holeFlopHash, bestHandPercentileAverage);

            return bestHandPercentileAverage;
        }

        public double GetHoleTurnPercentile(Hole hole, Turn turn)
        {
            Card[] cardsUsed = new Card[6]
            {
                hole.Card1,
                hole.Card2,
                turn.Card1,
                turn.Card2,
                turn.Card3,
                turn.Card4
            };

            Array.Sort(cardsUsed, new CardComparer());

            long holeTurnHash = GetMultiCardHash(cardsUsed);
            if (holeTurnPercentileMap_.ContainsKey(holeTurnHash))
                return holeTurnPercentileMap_[holeTurnHash];

            // 1) build up a full list of remaining deck cards
            Card[] deckCards = new Card[46];
            int cardIndex = 0;
            for (int suit = SuitValue.MinValue; suit <= SuitValue.MaxValue; ++suit)
            {
                for (int rank = RankValue.MinValue; rank <= RankValue.MaxValue; ++rank)
                {
                    Card newCard = new Card(rank, suit);
                    if (!cardsUsed.Contains(newCard, new CardEqualityComparer()))
                        deckCards[cardIndex++] = newCard;
                }
            }

            // 2) Build the possible rivers
            River[] possibleRivers = new River[46];
            int possibleRiverIndex = 0;
            for (int card0Index = 0; card0Index < 46; ++card0Index)
            {
                possibleRivers[possibleRiverIndex++] = new River(turn.Card1, turn.Card2, turn.Card3, turn.Card4, deckCards[card0Index]);
            }

            // 3) Find the best hand for the possible completed rivers
            double bestHandPercentileSum = 0;
            foreach (River possibleRiver in possibleRivers)
            {
                Hand bestHand = GetBestHand(hole, possibleRiver);

                // Now we need to build a list of all potential rival hands
                Card[] remainingCards = new Card[45];
                int remainingCardIndex = 0;
                for (int suit = SuitValue.MinValue; suit <= SuitValue.MaxValue; ++suit)
                {
                    for (int rank = RankValue.MinValue; rank <= RankValue.MaxValue; ++rank)
                    {
                        Card newCard = new Card(rank, suit);
                        if (!hole.Contains(newCard) && !possibleRiver.Contains(newCard))
                        {
                            remainingCards[remainingCardIndex++] = new Card(rank, suit);
                        }
                    }
                }

                Hole[] rivalHoles = new Hole[(45 * 44) / (2 * 1)];
                int rivalHoleIndex = 0;
                for (int card0Index = 0; card0Index < 44; ++card0Index)
                {
                    for (int card1Index = card0Index + 1; card1Index < 45; ++card1Index)
                    {
                        rivalHoles[rivalHoleIndex++] = new Hole(remainingCards[card0Index], remainingCards[card1Index]);
                    }
                }

                Hand[] allPossibleHands = new Hand[rivalHoles.Length + 1];
                int allPossibleHandsIndex = 0;
                foreach (Hole rivalHole in rivalHoles)
                {
                    allPossibleHands[allPossibleHandsIndex++] = GetBestHand(rivalHole, possibleRiver);
                }
                allPossibleHands[allPossibleHandsIndex] = bestHand;

                Array.Sort(allPossibleHands);

                // Get the index of our hand and thus calculate the percentile
                int bestHandIndex = -1;
                for (int handIndex = 0; handIndex < allPossibleHands.Length; ++handIndex)
                {
                    if (allPossibleHands[handIndex] == bestHand)
                    {
                        bestHandIndex = handIndex;
                        break;
                    }
                }

                double bestHandPercentile = (double)bestHandIndex / (double)(allPossibleHands.Length - 1);
                bestHandPercentileSum += bestHandPercentile;
            }

            double bestHandPercentileAverage = bestHandPercentileSum / (double)(possibleRivers.Length);


            holeTurnPercentileMap_.Add(holeTurnHash, bestHandPercentileAverage);

            return bestHandPercentileAverage;
        }

        public double GetHoleRiverPercentile(Hole hole, River river)
        {
            Card[] cardsUsed = new Card[7]
            {
                hole.Card1,
                hole.Card2,
                river.Card1,
                river.Card2,
                river.Card3,
                river.Card4,
                river.Card5
            };

            Array.Sort(cardsUsed, new CardComparer());

            long holeRiverHash = GetMultiCardHash(cardsUsed);
            if (holeTurnPercentileMap_.ContainsKey(holeRiverHash))
                return holeTurnPercentileMap_[holeRiverHash];

            // 1) build up a full list of remaining deck cards
            Card[] deckCards = new Card[45];
            int cardIndex = 0;
            for (int suit = SuitValue.MinValue; suit <= SuitValue.MaxValue; ++suit)
            {
                for (int rank = RankValue.MinValue; rank <= RankValue.MaxValue; ++rank)
                {
                    Card newCard = new Card(rank, suit);
                    if (!cardsUsed.Contains(newCard, new CardEqualityComparer()))
                        deckCards[cardIndex++] = newCard;
                }
            }

            // 2) Find the best hand for the possible completed rivers
            Hand bestHand = GetBestHand(hole, river);

            // Now we need to build a list of all potential rival hands
            Card[] remainingCards = new Card[45];
            int remainingCardIndex = 0;
            for (int suit = SuitValue.MinValue; suit <= SuitValue.MaxValue; ++suit)
            {
                for (int rank = RankValue.MinValue; rank <= RankValue.MaxValue; ++rank)
                {
                    Card newCard = new Card(rank, suit);
                    if (!hole.Contains(newCard) && !river.Contains(newCard))
                    {
                        remainingCards[remainingCardIndex++] = new Card(rank, suit);
                    }
                }
            }

            Hole[] rivalHoles = new Hole[(45 * 44) / (2 * 1)];
            int rivalHoleIndex = 0;
            for (int card0Index = 0; card0Index < 44; ++card0Index)
            {
                for (int card1Index = card0Index + 1; card1Index < 45; ++card1Index)
                {
                    rivalHoles[rivalHoleIndex++] = new Hole(remainingCards[card0Index], remainingCards[card1Index]);
                }
            }

            Hand[] allPossibleHands = new Hand[rivalHoles.Length + 1];
            int allPossibleHandsIndex = 0;
            foreach (Hole rivalHole in rivalHoles)
            {
                allPossibleHands[allPossibleHandsIndex++] = GetBestHand(rivalHole, river);
            }
            allPossibleHands[allPossibleHandsIndex] = bestHand;

            Array.Sort(allPossibleHands);

            // Get the index of our hand and thus calculate the percentile
            int bestHandIndex = -1;
            for (int handIndex = 0; handIndex < allPossibleHands.Length; ++handIndex)
            {
                if (allPossibleHands[handIndex] == bestHand)
                {
                    bestHandIndex = handIndex;
                    break;
                }
            }

            double bestHandPercentile = (double)bestHandIndex / (double)(allPossibleHands.Length - 1);

            holeTurnPercentileMap_.Add(holeRiverHash, bestHandPercentile);

            return bestHandPercentile;
        }

        public Hand CreateHand(Card card1, Card card2, Card card3, Card card4, Card card5)
        {
            int hash = GetFiveCardHash(card1, card2, card3, card4, card5);

            if (cachedHands_.ContainsKey(hash))
            {
                return cachedHands_[hash];
            }

            Hand newHand = new Hand(card1, card2, card3, card4, card5);
            cachedHands_.Add(hash, newHand);

            return newHand;
        }

        private Hand GetBestHand(Hole hole, River river)
        {
            Card[] availableCards = new Card[7] { hole.Card1, hole.Card2, river.Card1, river.Card2, river.Card3, river.Card4, river.Card5 };
            Array.Sort(availableCards, new CardComparer());
            long hash = GetMultiCardHash(availableCards);

            if (cachedBestHandsFrom7_.ContainsKey(hash))
                return cachedBestHandsFrom7_[hash];

            Hand[] possibleHands = new Hand[(7 * 6) / (2 * 1)];
            int possibleHandIndex = 0;
            for (int card0Index = 0; card0Index < 3; ++card0Index)
            {
                for (int card1Index = card0Index + 1; card1Index < 4; ++card1Index)
                {
                    for (int card2Index = card1Index + 1; card2Index < 5; ++card2Index)
                    {
                        for (int card3Index = card2Index + 1; card3Index < 6; ++card3Index)
                        {
                            for (int card4Index = card3Index + 1; card4Index < 7; ++card4Index)
                            {
                                possibleHands[possibleHandIndex++] = CreateHand(
                                    availableCards[card0Index],
                                    availableCards[card1Index],
                                    availableCards[card2Index],
                                    availableCards[card3Index],
                                    availableCards[card4Index]);
                            }
                        }
                    }
                }
            }

            Hand bestHand = possibleHands[0];
            for (int i = 1; i < possibleHands.Length; ++i)
            {
                if (possibleHands[i].CompareTo(bestHand) > 0)
                    bestHand = possibleHands[i];
            }

            cachedBestHandsFrom7_.Add(hash, bestHand);

            return bestHand;
        }

        private static int GetFiveCardHash(Card card1, Card card2, Card card3, Card card4, Card card5)
        {
            int hash = 0;
            hash += (int)card1.Suit << 0;
            hash += (int)card1.Rank << 2;
            hash += (int)card2.Suit << 6;
            hash += (int)card2.Rank << 8;
            hash += (int)card3.Suit << 12;
            hash += (int)card3.Rank << 14;
            hash += (int)card4.Suit << 18;
            hash += (int)card4.Rank << 20;
            hash += (int)card5.Suit << 24;
            hash += (int)card5.Rank << 26;

            return hash;
        }

        private static long GetMultiCardHash(Card[] cards)
        {
            if(cards.Length > 10)
                throw new InvalidOperationException("Too many cards for 64 bit hash");// 6 bits per card

            int bitIndex = 0;
            long hash = 0;
            foreach(Card card in cards)
            {
                hash += (int)card.Suit << bitIndex;
                bitIndex += 2;
                hash += (int)card.Rank << bitIndex;
                bitIndex += 4;
            }

            return hash;
        }

        public int NumHoleArchetypes { get { return holeArchetypes_.Length; } }

        private Dictionary<long, Hand> cachedBestHandsFrom7_;
        private Dictionary<int, Hand> cachedHands_ = new Dictionary<int, Hand>();

        private HoleArchetype[] holeArchetypes_;
        private Dictionary<int, double> holeFlopPercentileMap_;
        private Dictionary<long, double> holeTurnPercentileMap_;
        private Dictionary<long, double> holeRiverPercentileMap_;

        private const int numRiverSamples_ = 2048;
        private const int currentDataVersion_ = 2;

        private static Random random_ = new Random();
    }
}
